<?php
    /** 
     * TCPPC NetWork Server
     * network.php
     * Date:2024/07/31
     * @version 2.3.02
     * @author 布丁小才 zyn_games_email@yeah.net
     */

    //是否为命令行执行
    if (!php_sapi_name() === 'cli') {  
        echo '警告：请使用终端或命令提示符（cmd）运行此脚本';
        exit;
    }
    
    error_reporting(E_ALL ^ E_NOTICE);
    ob_implicit_flush();
    date_default_timezone_set('Asia/Shanghai');
    define('TCPPC_SERVER_VERSION','2.3.02');

    if (!file_exists(__DIR__.'\api.php')) {
        e('入口文件不存在！');
        exit;
    }

    require_once __DIR__.'\api.php';

    if (file_exists('data\tcppc.conf.json')) {
        $Config = json_decode(file_get_contents('data\tcppc.conf.json'),true);
    } else {
        $Config = array();
    }
    
	if (!is_array($Config)) {
		$Config = array();
	} 

	if (!array_key_exists('port',$Config) or !is_int($Config["port"])) {
		$Config["port"] = 7161;
	}

    if (!array_key_exists('log',$Config) or !is_bool($Config["log"])) {
		$Config["log"] = true;
	}

	if (!is_dir('pid')) {
	    //尝试创建目录  
	    if (!mkdir('pid', 0777, true)) {  
	        e("无法创建目录：pid");
            exit;
	    }  
	}

    if ($Config["log"]) {
        if (!is_dir('log')) {
            //尝试创建目录  
            if (!mkdir('log', 0777, true)) {  
                die("Unable to create log directory!");
            }  
        }
    
        $log_socket = fopen('log\log-'.date("Y-m-d_H-i-s").'.log','w');
    }

    $pid = getmypid();
	file_put_contents('pid\network.php.pid',$pid);

    //监听端口下所有数据包
    $server = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
    socket_set_option($server,SOL_SOCKET,SO_REUSEADDR, 1); //1表示接受所有的数据包
    socket_bind($server,'0.0.0.0',$Config["port"]);
    socket_listen($server);
    e('TCPPC NetWork Server');
    e('服务器当前时间:'.date('Y-m-d H:i:s'));
    e('服务器PID:'.$pid);
    e('服务器IP:'.gethostbyname(gethostname()));
    e('服务器名称:'.gethostname());
    e('服务器版本号:v'.TCPPC_SERVER_VERSION);
    $server_api = new TCPPC_SERVER_API();
    e('监听本地端口'.$Config["port"]);

    if (!method_exists($server_api,'websocket')) {
        e('警告：TCPPC_SERVER_API类中不存在websocket方法！');
        exit;
    }

    if (!method_exists($server_api,'websocket_welcome')) {
        e('警告：TCPPC_SERVER_API类中不存在websocket_welcome方法！');
        exit;
    }

    $ws = new websocket();
    $ws->sockets = array($server);
    $ws->server = $server;
    $http = new http();

    while (true) {
        $changes = $ws->sockets;
        $write = $except = null;
        //此函数监听是否有数据包到达服务器
        //否则会一直卡这里
        @socket_select($changes,$write,$except,null);
        foreach ($changes as $sock) {
            if ($sock == $server) {
                //接受连接
                $client = socket_accept($server);
                if ($client) {
                    //给新连接分配唯一的ID
                    $key = uniqid();
                    $ws->key_array[] = $key;
                    $ws->sockets[] = $client; //将新连接进来的socket存进连接池
                    $ws->users[$key] = array(
                        'socket' => $client, //记录新连接进来client的socket信息
                        'shou' => false    //标志该socket资源没有完成握手
                    );
                    e('成功接受新TCP连接');
                }
            } else {
                $len = 0;
                $buf = '';
                $data = '';

                //读取该socket的数据，注意：第二个参数是引用传参即接收数据，第三个参数是接收数据的长度
                do { 
                    $l = socket_recv($sock,$buf,1000,0);
                    $len += $l;
                    $data .= $buf;
                } while ($l==1000);

                if (strpos($data,'HTTP/1.1') == false) {
                    //可能是WS请求
                    $ws->net($sock,$len,$data);
                    continue;
                }

                $header_array = $http->headerdecode($data);
                if (
                    //GET请求
                    ($header_array['server']['REQUEST_METHOD'] == 'GET')
                    and
                    ($header_array['server']['PATH'] == $server_api->websocket_path)
                    and
                    ($header_array['header']['Connection'] == 'Upgrade')
                    and
                    ($header_array['header']['Upgrade'] == 'websocket')
                ) {
                    $ws->net($sock,$len,$data);
                } else {
                    //HTTP请求
                    $http_api = new http_api($sock);
                    $server_api->http($header_array['server']['PATH'],$header_array,$http_api);
                }
            }
        }
    }

    /**
     * 打印编码文本&记录日志
     * @param string $msg
     * @return void
     */
    function e($msg) {
        global $log_socket,$Config;
        //输出日志
        if ($Config["log"]) {
            fwrite($log_socket,'['.date('Y-m-d H:i:s').']'.$msg.PHP_EOL);
        }

        //编码处理并输出
        echo iconv('utf-8','gbk//IGNORE',$msg."\n");
    }


    //处理WS请求
    class websocket {
        public $sockets = array();
        public $server;
        public $key_array = array();
        public $users = array();
        private $sda=array();  //已接收的数据
        private $slen=array(); //数据总长度
        private $sjen=array(); //接收数据的长度
        private $ar=array();  //加密key
        private $n=array();

        public function net($sock,$len,$data) {
            //根据socket在user池里面查找相应的$k,即键ID
            $k = $this->search($sock);
            //如果接收的信息长度小于7，则该client的socket为断开连接
            if ($len < 7) {
                //给该client的socket进行断开操作，并在$this->sockets和$this->users里面进行删除
                $this->close($k);
                return;
            }

            if (!$this->users[$k]['shou']) {
                //如果没有握手，则进行握手处理
                $this->woshou($k,$data);
                //握手完成，发送Welcome消息
                $api = new websocket_send($k);
                global $server_api;
                $server_api->websocket_welcome($api);
            } else {
                //走到这里就是该client发送信息了，对接受到的信息进行uncode处理
                $buffer = $this->uncode($data,$k);
                if($buffer == false) {
                    return;
                }
                //如果不为空，则进行消息推送操作
                $this->newmsg($k,$buffer);
            }
        }

        public function search($sock) {
            foreach ($this->users as $k=>$v){
                if ($sock==$v['socket']) {
                    return $k;
                }  
            }
            return false;
        }

        private function woshou($k,$buffer){
            //截取Sec-WebSocket-Key的值并加密，其中$key后面的一部分258EAFA5-E914-47DA-95CA-C5AB0DC85B11字符串应该是固定的
            $buf = substr($buffer,strpos($buffer,'Sec-WebSocket-Key:')+18);
            $key = trim(substr($buf,0,strpos($buf,"\r\n")));
            $new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
     
            //按照协议组合信息进行返回
            $new_message = "HTTP/1.1 101 Switching Protocols\r\n";
            $new_message .= "Upgrade: websocket\r\n";
            $new_message .= "Sec-WebSocket-Version: 13\r\n";
            $new_message .= "Connection: Upgrade\r\n";
            $new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
            socket_write($this->users[$k]['socket'],$new_message,strlen($new_message));
     
            //对已经握手的client做标志
            $this->users[$k]['shou']=true;
            return true;
        }

        private function uncode($str,$key) {
            $mask = array();
            $data = '';
            $msg = unpack('H*',$str);
            $head = substr($msg[1],0,2);
            if ($head == '81' && !isset($this->slen[$key])) {
                $len=substr($msg[1],2,2);
                $len=hexdec($len);//把十六进制的转换为十进制
                if(substr($msg[1],2,2)=='fe'){
                    $len=substr($msg[1],4,4);
                    $len=hexdec($len);
                    $msg[1]=substr($msg[1],4);
                }else if(substr($msg[1],2,2)=='ff'){
                    $len=substr($msg[1],4,16);
                    $len=hexdec($len);
                    $msg[1]=substr($msg[1],16);
                }
                $mask[] = hexdec(substr($msg[1],4,2));
                $mask[] = hexdec(substr($msg[1],6,2));
                $mask[] = hexdec(substr($msg[1],8,2));
                $mask[] = hexdec(substr($msg[1],10,2));
                $s = 12;
                $n=0;
            }else if($this->slen[$key] > 0){
                $len=$this->slen[$key];
                $mask=$this->ar[$key];
                $n=$this->n[$key];
                $s = 0;
            }
     
            $e = strlen($msg[1])-2;
            for ($i=$s; $i<= $e; $i+= 2) {
                $data .= chr($mask[$n%4]^hexdec(substr($msg[1],$i,2)));
                $n++;
            }
            $dlen=strlen($data);
     
            if($len > 255 && $len > $dlen+intval($this->sjen[$key])){
                $this->ar[$key]=$mask;
                $this->slen[$key]=$len;
                $this->sjen[$key]=$dlen+intval($this->sjen[$key]);
                $this->sda[$key]=$this->sda[$key].$data;
                $this->n[$key]=$n;
                return false;
            }else{
                unset($this->ar[$key],$this->slen[$key],$this->sjen[$key],$this->n[$key]);
                $data=$this->sda[$key].$data;
                unset($this->sda[$key]);
                return $data;
            }
        }

        public function close($k) {
            //断开相应socket
            socket_close($this->users[$k]['socket']);
            //删除相应的user信息
            unset($this->users[$k]);
            $key_num = array_search($k,$this->key_array);
            unset($this->key_array[$key_num]);
            //重新定义sockets连接池
            $this->sockets=array($this->server);
            foreach($this->users as $v){
                $this->sockets[]=$v['socket'];
            }
            //输出日志
            e("用户$k 断开连接");
        }

        private function code($msg){
            $frame = array();
            $frame[0] = '81';
            $len = strlen($msg);
            if($len < 126){
                $frame[1] = $len<16?'0'.dechex($len):dechex($len);
            }else if($len < 65025){
                $s=dechex($len);
                $frame[1]='7e'.str_repeat('0',4-strlen($s)).$s;
            }else{
                $s=dechex($len);
                $frame[1]='7f'.str_repeat('0',16-strlen($s)).$s;
            }
            $frame[2] = $this->ord_hex($msg);
            $data = implode('',$frame);
            return pack("H*", $data);
        }

        private function ord_hex($data) {
            $msg = '';
            $l = strlen($data);
            for ($i= 0; $i<$l; $i++) {
                $msg .= dechex(ord($data[$i]));
            }
            return $msg;
        }

        //$k 发信息人的socketID 根据这个socketID可以查找相应的client进行消息推送，即指定client进行发送
        public function send($k,$nr){
		    $str = $this->code($nr);
		    e("服务器发送数据:$nr 给用户$k");
		    socket_write($this->users[$k]['socket'],$str,strlen($str));
        }

        private function newmsg($k,$msg) {
            e("服务器收到来自$k 的消息：$msg");
            $api = new websocket_send($k);
            global $server_api;
            $server_api->websocket(urldecode($msg),$api);
        }
    }

    /**
     * WebSocket响应层API
     */
    class websocket_send {
        public $k;
        public function __construct($k) {
            $this->k = $k;
        }

        /**
         * 对发送消息的用户回复
         * @param string $msg
         * @return void
         */
        public function send(string $msg) {
            global $ws;
            $ws->send($this->k,$msg);
        }

        /**
         * 断开服务器与发送消息的用户的连接
         * @return void
         */
        public function close() {
            global $ws;
            $ws->close($this->k);
        }

        /**
         * 向指定用户发送消息
         * @param string $msg
         * @param string $k
         * @return void
         */
        public function send_to_user(string $msg,string $k) {
            global $ws;
            $ws->send($k,$msg);
        }

        /**
         * 对所有用户发送消息
         * @param string $msg
         * @return void
         */
        public function send_to_all(string $msg) {
            global $ws;
            foreach($ws->users as $user) {
                $ws->send($ws->search($user["socket"]),$msg);
            }
            
        }

        /**
         * 断开指定用户的连接
         * @param string $k
         * @return void
         */
        public function close_to_user(string $k) {
            global $ws;
            $ws->close($k);
        }

        /**
         * 断开所有用户的连接
         * @return void
         */
        public function close_to_all() {
            global $ws;
            foreach($ws->users as $user) {
                $ws->close($ws->search($user["socket"]));
            }
            
        }
    }

    /**
     * HTTP响应层API
     */
    class http_api {
        /**
         * Cookie存储池
         * @var array
         */
        private $cookie = array();

        /**
         * 响应头存储池
         * @var array
         */
        private $header = array();

        /**
         * 发送HTTP响应包的目标用户
         * @var resource
         */
        private $sock;

        /**
         * 构造函数
         * @param resource $sock
         */
        public function __construct($sock) {
            $this->sock = $sock;
        }

        /**
         * 返回HTTP响应包
         * @param int $code
         * @param string $data
         * @return void
         */
        public function return(int $code,string $data) {
            global $http;
            socket_write($this->sock,$http->headerencode(
                $code,
                $data,
                ((count($this->cookie) == 0) ? null : $this->cookie),
                ((count($this->header) == 0) ? null : $this->header)
            ));
            socket_close($this->sock);
        }

        /**
         * 设置Cookie
         * @param string $name
         * @param string $value
         * @param int $expirte
         * @param string $path
         * @param string $domain
         * @param bool $secure
         * @param bool $httponly
         * @return bool
         */
        public function setcookie(string $name,string $value,int $expirte = 0,string $path = null,string $domain = null,bool $secure = false,bool $httponly = false) {
            $this->cookie[] = array(
                'name' => $name,
                'value' => $value,
                'expirte' => $expirte,
                'path' => $path,
                'domain' => $domain,
                'secure' => $secure,
                'httponly' => $httponly
            );
            return true;
        }

        /**
         * 设置响应头
         * @param string $name
         * @param string $value
         * @return bool
         */
        public function setheader(string $name,string $value) {
            $this->header[$name] = $value;
            return true;
        }
    }

    /**
     * HTTP解码&编码
     */
    class http {
        /**
         * HTTP状态码对应的信息
         * @var array
         */
        private $http_code = array(
            100 => 'Continue',
            101 => 'Switching Protocols',
            102 => 'Processing',
            200 => 'OK',
            201 => 'Created',
            202 => 'Accepted',
            203 => 'Non-Authoritative Information',
            204 => 'No Content',
            205 => 'Reset Content',
            206 => 'Partial Content',
            207 => 'Multi-status',
            208 => 'Already Reported',
            300 => 'Multiple Choices',
            301 => 'Moved Permanently',
            302 => 'Found',
            303 => 'See Other',
            304 => 'Not Modified',
            305 => 'Use Proxy',
            306 => 'Switch Proxy',
            307 => 'Temporary Redirect',
            400 => 'Bad Request',
            401 => 'Unauthorized',
            402 => 'Payment Required',
            403 => 'Forbidden',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            406 => 'Not Acceptable',
            407 => 'Proxy Authentication Required',
            408 => 'Request Time-out',
            409 => 'Conflict',
            410 => 'Gone',
            411 => 'Length Required',
            412 => 'Precondition Failed',
            413 => 'Request Entity Too Large',
            414 => 'Request-URI Too Large',
            415 => 'Unsupported Media Type',
            416 => 'Requested range not satisfiable',
            417 => 'Expectation Failed',
            418 => 'I\'m a teapot',
            422 => 'Unprocessable Entity',
            423 => 'Locked',
            424 => 'Failed Dependency',
            425 => 'Unordered Collection',
            426 => 'Upgrade Required',
            428 => 'Precondition Required',
            429 => 'Too Many Requests',
            431 => 'Request Header Fields Too Large',
            451 => 'Unavailable For Legal Reasons',
            500 => 'Internal Server Error',
            501 => 'Not Implemented',
            502 => 'Bad Gateway',
            503 => 'Service Unavailable',
            504 => 'Gateway Time-out',
            505 => 'HTTP Version not supported',
            506 => 'Variant Also Negotiates',
            507 => 'Insufficient Storage',
            508 => 'Loop Detected',
            511 => 'Network Authentication Required',
        );

        /**
         * 对响应头编码
         * @param int $code
         * @param string $data
         * @param array $cookie
         * @param array $user_header
         * @return string
         */
        public function headerencode(int $code,string $data,array $cookie = null,array $user_header = null) {
            $header = "HTTP/1.1 $code ".$this->http_code[intval($code)]."\r\n";
            $header .= "Server: TCPPC NewWork Server\r\n";
            $header .= "X-Server-Version: ".TCPPC_SERVER_VERSION."\r\n";
            if (isJson($data)) {
                $header .= "Content-Type: application/json;charset=utf-8\r\n";
            } else if (isHtml($data)) {
                $header .= "Content-Type: text/html;charset=utf-8\r\n";
            } else {
                $header .= "Content-Type: text/plain;charset=utf-8\r\n";
            }
            $header .= "Content-Length: ".strlen($data)."\r\n";
            $header .= "Connection: close\r\n";
            $header .= "Date: ".date('D, d M Y H:i:s \G\M\T',time())."\r\n";
            if (is_array($cookie)) {
                foreach ($cookie as $acookie) {
                    $acookie_header = "Set-Cookie: ".$acookie["name"]."=".$acookie["value"].";";
                    if ($acookie["expirte"] != 0) {
                        if ($acookie["expirte"] <= time()) {
                            $acookie_header .= " expires=".date('D, d M Y H:i:s \G\M\T',time()-60).";";
                        } else {
                            $acookie_header .= " expires=".date('D, d M Y H:i:s \G\M\T',$acookie["expirte"]).";";
                        }
                    }
                    if ($acookie["path"] != null) {
                        $acookie_header .= " path=".$acookie["path"].";";
                    }
                    if ($acookie["domain"] != null) {
                        $acookie_header .= " domain=".$acookie["domain"].";";
                    }
                    if ($acookie["secure"] == true) {
                        $acookie_header .= " secure;";
                    }
                    if ($acookie["HttpOnly"] == true) {
                        $acookie_header .= " HttpOnly;";
                    }
                    $acookie_header .= " \r\n";
                    $header .= $acookie_header;
                }
            }
            if (is_array($user_header)) {
                foreach ($user_header as $key => $value) {
                    $header .= $key.": ".$value."\r\n";
                }
            }
            $header .= "\r\n";
            $header .= $data;
            return $header;
        }

        /**
         * 对请求头解码
         * @param mixed $data
         * @return array
         */
        public function headerdecode($data) {
            //解刨头数据
            $data_array = explode("\r\n",$data);
            //忽略无效数据
            $data_array = array_filter($data_array);
            //数据头第一行解刨
            $header1_array = explode(" ",$data_array[0]);
            $header1_array = array_filter($header1_array);

            //解刨其他头数据
            for (
                $i=1;
                $i < count($data_array);
                $i++
            ) {
                $header_data_temp_array = explode(": ",$data_array[$i]);
                $header_array[urldecode($header_data_temp_array[0])] = urldecode($header_data_temp_array[1]);
            }
            $header_array = is_array($header_array) ? array_filter($header_array) : array();
            unset($header_data_temp_array,$i);

            //获取请求方法 请求路径
            $method = $header1_array[0];
            $request_url = 'http://'.$header_array["Host"].$header1_array[1];
            $url_path = urldecode(parse_url($request_url,PHP_URL_PATH));

            //获取查询字符串数组
            $query_string = parse_url($request_url,PHP_URL_QUERY);
            if ($query_string != '') {
                $query_array = explode("&",$query_string);
                $query_array = array_filter($query_array);
                for (
                    $i=0;
                    $i < count($query_array);
                    $i++
                ) {
                    $query_data_temp_array = explode("=",$query_array[$i]);
                    $query_data_array[urldecode($query_data_temp_array[0])] = urldecode($query_data_temp_array[1]);
                }
                $query_array = is_array($query_data_array) ? array_filter($query_data_array) : array();
                unset($query_data_temp_array,$i,$query_data_array);
            } else {
                $query_array = array();
            }

            //获取Cookie（若有）
            $cookie_array = explode(";",$header_array["Cookie"]);
            $cookie_array = array_filter($cookie_array);

            for (
                $i=0;
                $i < count($cookie_array);
                $i++
            ) {
                $cookie_data_temp_array = explode("=",$cookie_array[$i]);
                $cookie_data_array[urldecode($cookie_data_temp_array[0])] = urldecode($cookie_data_temp_array[1]);
            }
            $cookie_array = is_array($cookie_data_array) ? array_filter($cookie_data_array) : array();
            unset($cookie_data_temp_array,$i,$cookie_data_array);

            //获取POST数据（若有）
            if ($method == "POST") {
                if ($header_array["Content-Type"] == 'application/x-www-form-urlencoded') {
                    $post_array = explode("&",$data_array[count($data_array)]);
                    $post_array = array_filter($post_array);

                    for (
                        $i=0;
                        $i < count($post_array);
                        $i++
                    ) {
                        $post_data_temp_array = explode("=",$post_array[$i]);
                        $post_data_array[urldecode($post_data_temp_array[0])] = urldecode($post_data_temp_array[1]);
                    }
                    $post_array = is_array($post_data_array) ? array_filter($post_data_array) : array();
                    unset($post_data_temp_array,$i,$post_data_array);
                } else if ($header_array["Content-Type"] == 'application/json') {
                    $post_array = json_decode($data_array[count($data_array)-1],true);
                } else if ($header_array["Content-Type"] == 'text/plain') {
                    $post_array = $data_array[count($data_array)-1];
                }
            }

            //整理数据
            $http_data = array(
                'get' => $query_array,
                'post' => $post_array,
                'cookie' => $cookie_array,
                'server' => array(
                    'QUERY_STRING' => $query_string,
                    'REQUEST_METHOD' => $method,
                    'REQUEST_URI' => $request_url,
                    'SERVER_PROTOCOL' => 'HTTP/1.1',
                    'SERVER_NAME' => 'TCPPC Network Server',
                    'HTTP_HOST' => $header_array["Host"],
                    'HTTP_USER_AGENT' => $header_array["User-Agent"],
                    'HTTP_ACCEPT' => $header_array["Accept"],
                    'HTTP_ACCEPT_LANGUAGE' => $header_array["Accept-Language"],
                    'HTTP_ACCEPT_ENCODING' => $header_array["Accept-Encoding"],
                    'HTTP_COOKIE' => $header_array["Cookie"],
                    'HTTP_CONNECTION' => $header_array["Connection"],
                    'REMOTE_ADDR' => parse_url($request_url,PHP_URL_HOST),
                    'REMOTE_PORT' => parse_url($request_url,PHP_URL_PORT),
                    'REQUEST_TIME' => time(),
                    'CONTENT_TYPE' => $header_array["Content-Type"],
                    'PATH' => $url_path
                ),
                'request' => array_merge(
                    $query_array,
                    ((isset($post_array) and is_array($post_array)) ? $post_array : array()),
                    $cookie_array
                ),
                'header' => $header_array
            );

            return $http_data;
        }
    }

    /**
     * 判断目标字符串是否为JSON
     * @param mixed $string
     * @return bool
     */
    function isJson($string) {
        json_decode($string);
        return (json_last_error() == JSON_ERROR_NONE);
    }
     
    /**
     * 判断目标字符串是否为HTML
     * @param mixed $string
     * @return bool
     */
    function isHtml($string) {
        return ((preg_match('/<\s*html\s*>/i', $string) === false) ? false : true);
    }